<?php
if (realpath(__FILE__) == realpath($_SERVER['SCRIPT_FILENAME'])) {exit('No Direct Access!');}

/*
 +-----------------------------------------------------------------------+
 | Copyright (c) 2009 , Ersin Güvenç                                     |
 | All rights reserved.                                                  |
 |                                                                       |
 | EasyRest Server Library v 1.1 beta                                    |
 | PHP VERSION 5                                                         |
 | LICENSE: GPL                                                          |
 |                                                                       |
 | Redistribution and use in source and binary forms, with or without    |
 | modification, are permitted provided that the following conditions    |
 | are met:                                                              |
 |                                                                       |
 | o Redistributions of source code must retain the above copyright      |
 |   notice, this list of conditions and the following disclaimer.       |
 | o Redistributions in binary form must reproduce the above copyright   |
 |   notice, this list of conditions and the following disclaimer in the |
 |   documentation and/or other materials provided with the distribution.|
 | o The names of the authors may not be used to endorse or promote      |
 |   products derived from this software without specific prior written  |
 |   permission.                                                         |
 |                                                                       |
 |  This program is free software: you can redistribute it and/or modify |
 |  it under the terms of the GNU General Public License as published by |
 |  the Free Software Foundation, either version 3 of the License, or    |
 |  (at your option) any later version.                                  |
 |                                                                       |
 |  This program is distributed in the hope that it will be useful,      |
 |  but WITHOUT ANY WARRANTY; without even the implied warranty of       |
 |  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the        |
 |  GNU General Public License for more details.                         |
 |                                                                       |
 |  You should have received a copy of the GNU General Public License    |
 |  along with this program. If not, see <http://www.gnu.org/licenses/>  |
 |                                                                       |
 +-----------------------------------------------------------------------+
 | Author: Ersin Güvenç <eguvenc at gmail.com>                           |
 +-----------------------------------------------------------------------+
*/

/**
 * @category  WebServices
 * @package   EasyRest
 * @author    Ersin Güvenç <eguvenc@gmail.com>
 * @copyright 2009 Ersin Güvenç
 * @license   http://www.opensource.org/licenses/gpl-3.0.html GPL
 * @version   1.0
 * @filesource
 * @link      http://www.develturk.com
 * @see       
 * @since     1.0
 * 
 */
 
// EasyRest Server Class
include ('restCommon.inc.php');

/**
* Index var for indexing predefined functions.
* @global index
*/   
$GLOBALS['index'] = "index";

/**
* Supported parameter data types.
* @global types
*/   
$GLOBALS['types'] = array(
                          'int'     => 'integer',
                          'string'  => 'string',
                          'boolean' => 'boolean',
                          'bool'    => 'boolean',  //second type writing.
                          'float'   => 'float',
                          'array'   => 'array',
                          );

require ('restXML.php');


Class Server_Exception extends Easyrest_Exception{} 

/**
* Rest_service abstract class.
* @version 1.0
*/   
abstract class Rest_Service 
{
    //base protected functions
    abstract protected function define(array $functions);
    
    //start serving
    abstract protected function serve();
    
    //return response with xml_serializer.
    abstract protected function serializeXMLResponse(array $array);

}

/**
* Server Config class
* Set server config variables like apikey
* @version 1.0
*/
Class Config
{
    /**
    * store config vars into array
    * @var array 
    */                  
    public $settings = array();

    /**
    * static object instance
    * @var object 
    */ 
    private static $instance = NULL;
    
    //disable construct
    private function __construct()
    {
        global $config;
        
        //* Set iv size e.g: abc123+=. it must be equal with client iv. 
        $this->settings['iv'] = $config['common']['iv'];
    }
    
    /**
    * Get instance of object
    * 
    * If there is not already an instance of this class,
    * instantiate one.
    * @version 1.0
    * @return object instance
    */
    static public function getInstance()
    {
        if (self::$instance == NULL)
        {
            self::$instance = new Config;
        }
 
        return self::$instance;
    }
    
    /**
    * Get config item
    * @version 1.0
    * @param string $item
    */
    function apikey_function($user_callback_func)
    {
        $this->settings['apikey_function'] = $user_callback_func;
    }
    
    /**
    * Get config item
    * @version 1.0
    * @param string $item
    */
    function item($item)
    {
        return $this->settings[$item];
    }

} //end class.


/**
* Server class
* Get requests and parse all functions
* @version 1.0
*/
Class Server extends Rest_Service 
{
    /**
    * Set server url
    * @var string 
    */
    public $ServerUrl = ""; 

    /**
    * Build server functions from outside
    * @var array 
    */
    private $ServerFunctions = array();

    /**
    * Set defined functions on the server
    * @var array 
    */
    private $DefinedFunctions = array();

    /**
    * Get request['function'] and set
    * @var string 
    */
    private $function;

    /**
    * Function parameters
    * @var array 
    */
    private $params = array();

    /**
    * Filtered function parameters
    * @var array 
    */
    private $SecureParams = array();

    /**
    * Filtered function
    * @var string 
    */
    private $SecureFunction;
    
    
    /**
    * Client id
    * @var integer
    */
    public $id;
    
    /**
    * Client apikey
    * @var string 
    */
    public $apikey;

    /**
    * Set server incoming request method (post or get)
    * @var string 
    */
    private $req_method;

    /**
    * Instance of config class
    * @var object 
    */
    public $config;

    /**
    * Build config object          
    * @version 1.0
    */
    function __construct()
    {
       $this->config = Config::getInstance();
    }
    
    /**
    * Server version.          
    * @version 1.0
    */
    function version(){ return "EasyRest Server v1.0";}
    
    /**
    * Initialize server library class          
    * @version 1.0
    */
    private function initServer()
    {
        //parse incoming request variables.
        $this->getRequest();
           
        //check api key
        $api_check = $this->callback_user_check_apikey();
        
        if(!$api_check)
        throw new Server_Exception('Server error wrong api key!');
        
        if($this->function == $GLOBALS['index'])
        {
          $this->indexAll();
          
          return FALSE;
          
        } else {
          
          $this->SecureFunction = $this->ValidateFunction();
          $this->SecureParams   = $this->PIF(); //Parameters Input Filter  
          //..set params
          
        }
    }
    
    /**
    * Get request variables from client request
    * 
    * @access private          
    * @version 1.0
    */      
    private function getRequest()
    {
        if(isset($_POST['function']) && $_SERVER['REQUEST_METHOD'] == 'POST') 
        {
            $this->req_method = "POST";
       
        } elseif(isset($_GET['function']) && $_SERVER['REQUEST_METHOD'] == 'GET')
        {   
            $this->req_method = "GET";
        }
        /*
                case 'PUT':
                break;
                
                case 'DELETE':
                break;
                
                case 'INSERT':
                break;,
        */
        
        //print_r($_REQUEST['params']); 
        //exit; 
        
        if( ! isset($_REQUEST['params'])) 
        {
           $this->params = array(); 
        } 
        else 
        {
           $this->params = $_REQUEST['params'];  
        }
        
        $this->function = $_REQUEST['function'];
        
    }
    
    /**
    * Get apikey current client apikey
    * 
    * @access public static          
    * @version 1.0
    */
    public static function get_apikey()
    {
        return $_REQUEST['apikey']; 
    }
    
    /**
    * Get current client id
    * 
    * @access public static          
    * @version 1.0
    */
    public static function get_id()
    {
        return $_REQUEST['id']; 
    }
    
    /**
    * Run user check apikey function
    *  
    * @access private          
    * @version 1.0
    * @uses preg_match
    * @uses explode
    * @uses call_user_func
    * @return boolean if apikey true return true
    */
    private function callback_user_check_apikey()
    {
        $func_name = $this->config->item('apikey_function');
        
        if (preg_match('/\::/', $func_name))
        {
            $func = explode('::',$func_name);
            $class  = $func[0];
            $method = $func[1];
            $obj = new $class(); 
            $ret = call_user_func(array($obj,$method));
        
        } else
        {
            if(!function_exists($func_name))
            throw new Server_Exception('User check apikey function doesn\'t exist!');
            
            $ret = call_user_func($func_name);
        }
        
        return $ret;
    } 
    
    
    /**
    * Encrypt user apikey
    * Blowfish encrypt method
    * 
    * @access public static
    * @param string $secretkey
    * @param string $apikey
    * @version 1.0
    * @return mixed encrypted_apikey
    */ 
    public static function encrypt($secretkey, $apikey)
    {             
        require (PEAR_LIB.'Crypt/Blowfish.php');
        
        $bf =& Crypt_Blowfish::factory('cbc');

        if(PEAR::isError($bf))
        throw new Server_Exception($bf->getMessage());
        
        $iv = Config::getInstance()->item('iv'); //this function should be static.
        
        $bf->setKey($secretkey, $iv);
        $encrypted_apikey = $bf->encrypt($apikey);
        
        if (PEAR::isError($encrypted_apikey))
        throw new Server_Exception($encrypted_apikey->getMessage());
        
        return $encrypted_apikey;                                
    }
    
    
    /**
    * Define server functions
    * @access public
    * @param array $functions defined functions from user          
    * @version 1.0
    */ 
    public function define(array $functions)
    {                            
        if( sizeof($functions) < 1)
        {
            throw new Server_Exception('Please define at least one function on the 
                                        REST server!');
        }else{
            
            $this->ServerFunctions = $functions;
            
            foreach($functions as $key=>$val)
            {
                $this->DefinedFunctions[$key] = $key; 
            }
        }
    }
    
    /**
    * Parse rule of params for xss preventation
    * 
    * @access private
    * @param string $rule         
    * @version 1.0
    * @uses preg_match
    * @return array $data rule and val
    */ 
    private function parseRule($rule)
    {
        if (preg_match("/(.*?)\[(.*?)\]/", $rule, $match))
        {
            $data['rule'] = $match[1];
            $data['val']  = $match[2];
        }
        
        if(empty($data['val']) && $data['val'] == NULL) 
        throw new Server_Exception('Filter rule value must be in [brace]!');
         
        return $data;
    }
    
    /**
    * Explode items from "|" characters if first value = 0 
    * return integer,boolean,string,float,array.
    * 
    * @access private
    * @param string $val         
    * @version 1.0
    * @uses strcasecmp
    * @uses preg_match
    * @uses explode
    * @return array $items
    */ 
    private function parseValue($val)
    {
        if(empty($val)) { $items[0] = 'empty'; return $items;  }
        if(strcasecmp($val,'false') == 0 || $val == FALSE) { $items[0] = 'false'; return $items;  }
        
        if (!preg_match('/\|/', $val))
        {
            if(!isset($GLOBALS['types'][$val]))
            throw new Server_Exception('This data type does not support!');

            $items[0] = $val;
            return $items;
        } 
        
        $items = explode("|",$val);
        
        return $items;
    }
    
    
    /**
    * PIF (Parameters Input Filter)               
    * Filter all paramaters -  with PHP5 Input Filter   (html not allowed)
    * 
    * allowing html is a security risk use filter[false] for allowing html
    * and you MUST use your own xss filter.
    * SERVER SUPPORTED TYPES : int, string, boolean, float, array.
    * 
    * @access private
    * @link http://devzone.zend.com/article/1113          
    * @link http://phpro.org/tutorials/Filtering-Data-with-PHP.html          
    * @version 1.1
    * @uses is_array
    * @uses sizeof
    * @uses filter_var
    * @uses substr
    * @uses strcasecmp
    * @uses filter_var_array
    * @return array $func_params | false
    */
    private function PIF()
    {   
        if( ! is_array($this->params)) return $this->params;
        //throw new Server_Exception('Function parameters must be array!');
        
        $server_params = $this->ServerFunctions[$this->SecureFunction]['params'];
        //print_r($server_params); exit;
        //print_r($this->params);
        //print_r($this->ServerFunctions[$this->function]['function']);
        //exit;
        
        $called_function = $this->SecureFunction;
        
        //if server arguments and client arguments not equal throw an error!
        if(sizeof($this->params) !== sizeof($server_params))
        throw new Server_Exception('Client and Server defined arguments not equal!');

        $func_params = array(); 
        foreach($server_params as $key=>$val)
        {           
            $value = $this->parseValue($val);  //Array ( [0] => int [1] => min[7] [2] => max[77] ) 
            //print_r($value);
           
            $xss_msg = 'Security filter error occured on '.$called_function.'
            parameter number '.$key.'.it must be ';
            
            switch ($value[0]) 
            {               
                case 'int':
                    if(empty($value[1]))
                    {   
                        $param = filter_var($this->params[$key],
                                                FILTER_VALIDATE_INT);
                         
                         
                                                
                    }elseif(empty($value[2])){
                        
                        $param = filter_var($this->params[$key],                                                                                              FILTER_VALIDATE_INT,
                                                array(
                                                    'min_range'=> $value[1], 
                                                    'max_range'=> 99
                                                    ));                                    
                    }else{
                    
                        $param = filter_var($this->params[$key],                                                                                              FILTER_VALIDATE_INT,
                                                array(
                                                    'min_range'=> $value[1], 
                                                    'max_range'=> $value[2]
                                                    ));
                    }
                    
                    if(!$param)
                    throw new Server_Exception($xss_msg.'INTEGER!');
                         
                    $func_params[$key] = (int)$param;
                    
                    break;
                    
                case 'string':
                    $string = $this->params[$key];
                    $filter = TRUE;
                
                    # Check VALUE 1 #
                    if(!empty($value[1]))
                    {       
                        //maxchar /[int]
                        if(substr($value[1], 0, 7) == 'maxchar')
                        {
                            $rule   = $this->parseRule($value[1]); 

                            //use isset instead of strlen and up to performance
                            if(isset($string{(int)$rule['val']}))
                            {
                                throw new Server_Exception('Parameter (string) 
                                                                rule doesn\'t allow bigger than
                                                                '.(int)$rule['val'].' characters.');
                            } 
                        }
                        
                        ###  WARNING! If its false Easyrest INPUT_FILTER will be OFF  ###
                        if(substr($value[1], 0, 6) == 'filter')
                        {
                            $rule   = $this->parseRule($value[1]);
                            if(strcasecmp($rule['val'], 'false') == 0)
                            $filter = FALSE; //WARNING!! FILTER OFF!
                             
                        }
                        
                    }
                    
                    # Check VALUE 2 # 
                    if(!empty($value[2]))
                    {
                        //maxchar
                        if(substr($value[2], 0, 7) == 'maxchar')
                        {
                            $rule   = $this->parseRule($value[2]); 

                            //use isset instead of strlen and up to performance
                            if(isset($string{(int)$rule['val']}))
                            {
                                throw new Server_Exception('Parameter (string) 
                                                                rule doesn\'t allow bigger than
                                                                '.(int)$rule['val'].' characters.');
                            }
                             
                        }
                        
                        ###  WARNING! If its false Easyrest INPUT_FILTER will be OFF  ###
                        if(substr($value[2], 0, 6) == 'filter')
                        {
                            $rule   = $this->parseRule($value[2]);
                            if(strcasecmp($rule['val'], 'false') == 0)
                            $filter = FALSE; //WARNING!! FILTER OFF!
                        }
                        
                    }
          
                    $param = $string;
                    
                    if($filter) {
                    $param = filter_var($string,FILTER_SANITIZE_STRING, 
                                                FILTER_FLAG_STRIP_HIGH|FILTER_FLAG_STRIP_LOW);
                    }
                                                
                    if(!$param)
                    throw new Server_Exception($xss_msg.'STRING!');
                                                     
                    $func_params[$key] = (string)$param;     
                    
                    break;
                    
                case 'boolean':
                
                    if (strcasecmp($this->params[$key], 'true') == 0 
                    || $this->params[$key] == 1 
                    || ($this->params[$key] == TRUE && strcasecmp($this->params[$key], 'false') ))
                    {
                        $func_params[$key] = TRUE; 
                    } else {
                        $func_params[$key] = FALSE; 
                    }
                    
                    break;
                    
                case 'bool':
                
                    if (strcasecmp($this->params[$key], 'true') == 0 
                    || $this->params[$key] == 1 
                    || ($this->params[$key] == TRUE && strcasecmp($this->params[$key], 'false') ))
                    {
                        $func_params[$key] = TRUE; 
                    } else {
                        $func_params[$key] = FALSE; 
                    }
                    
                    break;
                
                case 'float':
                
                    $param = filter_var($this->params[$key],FILTER_VALIDATE_FLOAT);
                                                
                    if(!$param)
                    throw new Server_Exception($xss_msg.'FLOAT!');
                                                     
                    $func_params[$key] = (float)$param;
                    
                    break;
                
                case 'array':
                
                    $param = filter_var_array((array)$this->params[$key],FILTER_SANITIZE_STRING);
       
                    if(!$param)
                    throw new Server_Exception($xss_msg.'ARRAY!');
                                                     
                    $func_params[$key] = (array)$param;
                                              
                    break;
                    
                case 'empty':           
                    $func_params[$key] = '';
                break;
                
                case 'false':
                    $func_params[$key] = FALSE;
                break;
                
            } //end switch.
        } //end foreach.
        
        //$this->parseParams($func_params);
        //print_r($func_params); 
        //exit;
        
        //Sanitizied parameters.
        return $func_params;
    }
                    
    /**
    * Validate function name.
    * 
    * @access private         
    * @version 1.0
    * @uses filter_var
    * @uses ereg
    * @return string function
    */
    private function ValidateFunction()
    {
        //use isset instead of in_array performance up to %30.. 
        if(!isset($this->DefinedFunctions[$this->function])) 
        throw new Server_Exception('Function not defined on the server!');
        
        $function = filter_var($_REQUEST['function'],FILTER_SANITIZE_STRING); //destroy html tags
        
        if($function === FALSE)
        throw new Server_Exception('Server security error!');
        
        //check function name it must be  blabla.blabla and first and last word
        //word must be between 1-30 charecters.
        if(!ereg("^[-A-Za-z0-9_]{1,30}$*[.][-A-Za-z0-9_]{1,30}$", $function))
        {
           throw new Server_Exception('Function name should be strict 
                                       , it must be between 1-60 characters!');  
        }
        
        return $function;
    }
    
    /**
    * Indexall functions
    * Indexall defined functions on the server
    *  
    * @access private        
    * @version 1.0
    * @uses print_r
    */
    private function indexAll()
    {
         foreach($this->ServerFunctions as $key=>$val) 
         {
            echo '<b>EasyRest Server Info:</b> <br />';
            echo '<b>Defined Functions:</b> <br />';
            print $key.print_r($val).'<br />';
            
            echo '<br /><br />'; 
         }
         
         echo '<b>Version:</b> '.$this->version(); 
    }
    
    /**
    * Serialize xml response
    * Convert array values to xml produce xml
    * content
    *  
    * @access protected
    * @param array $array
    * @uses Rest_xml::getInstance
    * @uses header           
    * @version 1.0
    */
    function serializeXMLResponse(array $array)
    {
        $xml = Rest_xml::getInstance();
        $xml->_setCommand($this->SecureFunction);
        $xml_content = $xml->serialize($array);
        
        header('Content-type: text/xml');
        echo $xml_content;
    }

    /**
    * Call functions 
    * call user defined functions on the server
    * 
    * @access private       
    * @version 1.0
    * @uses preg_match
    * @uses explode
    * @uses filter_var
    * @uses method_exists
    * @uses call_user_func_array
    * @return array $data 
    */                                                    
    private function callUserFunction()
    {
        $function = $this->ServerFunctions[$this->SecureFunction]['function'];
        $params   = $this->ServerFunctions[$this->SecureFunction]['params'];
        //$func_params_array = array_values($params);
        
        if (preg_match('/\::/', $function))  //object reflection
        {
            $func = explode('::',$function);
        
            $class  = filter_var($func[0],FILTER_SANITIZE_STRIPPED);
            $method = filter_var($func[1],FILTER_SANITIZE_STRIPPED);
            
            //reflection
            
            if(!method_exists($class,$method))
            throw new Server_Exception('Server error ' . '"'.$method.'"' . ' function doesn\'t exists!');
            
            $reflector = new ReflectionMethod($class,$method); 
            $number_of_params = $reflector->getNumberOfParameters();
            //getNumberOfRequiredParameters();

            if(count($params) !== $number_of_params)
            throw new Server_Exception('Server defined arguments and '.'"'.$method.'"'.'
             function arguments not equal !'); 
            
            
            $OB = new $class(); 
            $data = call_user_func_array(array($OB, $method), $this->SecureParams);
        
        } else                                   //function reflection
        {
            $func_name = filter_var($function,FILTER_SANITIZE_STRIPPED);
            
            if(!function_exists($func_name))
            throw new Server_Exception('Server error ' . '"'.$func_name.'"' . ' function doesn\'t exists!'); 
            
            //reflection
            
            $reflector = new ReflectionFunction($func_name);
            $number_of_params =  $reflector->getNumberOfParameters();
            
            if(count($params) !== $number_of_params)
            throw new Server_Exception('Server defined arguments and '.'"'.$method.'"'.'
             function arguments not equal !');
            
            $data = call_user_func_array($func_name, $this->SecureParams);
        }
        
        return $data;
    }
               
    /**
    * Initialize and run Easyrest Web Service.
    *  
    * @access public          
    * @version 1.0
    */ 
    function serve()
    {
        try
        {   

            $this->initServer();
            
            $data = $this->callUserFunction();
            
            //$struct_data = array($this->SecureFunction => $data);
        
            /*  test
            $data['params'][] = array('params' => $this->SecureParams,
                                      'function_secure'=> $this->SecureFunction,
                                      'function'=> $this->ServerFunctions[$this->SecureFunction]['function']);
            */
                   
            $this->serializeXMLResponse($data);

        } catch (Server_Exception $e)
        {
            
            $data = array('fault' => 
                          array(
                                'string' => $e->getMessage(),
                                'line'   => $e->getLine())
                                );
                                
            self::serializeXMLResponse($data);
            
        }
         
    }

} //end class.


?>
